// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/push_messaging/push_messaging_message_filter.h"

#include <string>
#include <vector>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_storage.h"
#include "content/common/push_messaging_messages.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/permission_manager.h"
#include "content/public/browser/permission_type.h"
#include "content/public/browser/push_messaging_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/child_process_host.h"
#include "content/public/common/console_message_level.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/push_messaging_status.h"
#include "third_party/WebKit/public/platform/modules/push_messaging/WebPushPermissionStatus.h"

namespace content {

// Service Worker database keys. If a registration ID is stored, the stored
// sender ID must be the one used to register. Unfortunately, this isn't always
// true of pre-InstanceID registrations previously stored in the database, but
// fortunately it's less important for their sender ID to be accurate.
const char kPushSenderIdServiceWorkerKey[] = "push_sender_id";
const char kPushRegistrationIdServiceWorkerKey[] = "push_registration_id";

namespace {

    // Chrome currently does not support the Push API in incognito.
    const char kIncognitoPushUnsupportedMessage[] = "Chrome currently does not support the Push API in incognito mode "
                                                    "(https://crbug.com/401439). There is deliberately no way to "
                                                    "feature-detect this, since incognito mode needs to be undetectable by "
                                                    "websites.";

    // These UMA methods are only called from IO thread, but it would be acceptable
    // (even though slightly racy) to call them from UI thread as well, see
    // https://groups.google.com/a/chromium.org/d/msg/chromium-dev/FNzZRJtN2aw/Aw0CWAXJJ1kJ
    void RecordRegistrationStatus(PushRegistrationStatus status)
    {
        DCHECK_CURRENTLY_ON(BrowserThread::IO);
        UMA_HISTOGRAM_ENUMERATION("PushMessaging.RegistrationStatus", status,
            PUSH_REGISTRATION_STATUS_LAST + 1);
    }

    void RecordUnregistrationStatus(PushUnregistrationStatus status)
    {
        DCHECK_CURRENTLY_ON(BrowserThread::IO);
        UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationStatus", status,
            PUSH_UNREGISTRATION_STATUS_LAST + 1);
    }

    void RecordGetRegistrationStatus(PushGetRegistrationStatus status)
    {
        DCHECK_CURRENTLY_ON(BrowserThread::IO);
        UMA_HISTOGRAM_ENUMERATION("PushMessaging.GetRegistrationStatus", status,
            PUSH_GETREGISTRATION_STATUS_LAST + 1);
    }

    // Curries the |success| and |p256dh| parameters over to |callback| and
    // posts a task to invoke |callback| on the IO thread.
    void ForwardEncryptionInfoToIOThreadProxy(
        const PushMessagingService::EncryptionInfoCallback& callback,
        bool success,
        const std::vector<uint8_t>& p256dh,
        const std::vector<uint8_t>& auth)
    {
        DCHECK_CURRENTLY_ON(BrowserThread::UI);
        BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
            base::Bind(callback, success, p256dh, auth));
    }

    // Returns whether |sender_info| contains a valid application server key, that
    // is, a NIST P-256 public key in uncompressed format.
    bool IsApplicationServerKey(const std::string& sender_info)
    {
        return sender_info.size() == 65 && sender_info[0] == 0x04;
    }

    // Returns sender_info if non-empty, otherwise checks if stored_sender_id
    // may be used as a fallback and if so, returns stored_sender_id instead.
    //
    // This is in order to support the legacy way of subscribing from a service
    // worker (first subscribe from the document using a gcm_sender_id set in the
    // manifest, and then subscribe from the service worker with no key).
    //
    // An empty string will be returned if sender_info is empty and the fallback
    // is not a numeric gcm sender id.
    std::string FixSenderInfo(const std::string& sender_info,
        const std::string& stored_sender_id)
    {
        if (!sender_info.empty())
            return sender_info;
        if (base::ContainsOnlyChars(stored_sender_id, "0123456789"))
            return stored_sender_id;
        return std::string();
    }

} // namespace

struct PushMessagingMessageFilter::RegisterData {
    RegisterData();
    RegisterData(const RegisterData& other) = default;
    bool FromDocument() const;
    int request_id;
    GURL requesting_origin;
    int64_t service_worker_registration_id;
    PushSubscriptionOptions options;
    // The following member should only be read if FromDocument() is true.
    int render_frame_id;
};

// Inner core of this message filter which lives on the UI thread.
class PushMessagingMessageFilter::Core {
public:
    Core(const base::WeakPtr<PushMessagingMessageFilter>& io_parent,
        int render_process_id);

    // Public Register methods on UI thread --------------------------------------

    // Called via PostTask from IO thread.
    void RegisterOnUI(const RegisterData& data);

    // Public Unregister methods on UI thread ------------------------------------

    // Called via PostTask from IO thread.
    void UnregisterFromService(int request_id,
        int64_t service_worker_registration_id,
        const GURL& requesting_origin,
        const std::string& sender_id);

    // Public GetPermission methods on UI thread ---------------------------------

    // Called via PostTask from IO thread.
    void GetPermissionStatusOnUI(const GURL& requesting_origin,
        bool user_visible,
        int request_id);

    // Public helper methods on UI thread ----------------------------------------

    // Called via PostTask from IO thread. The |io_thread_callback| callback
    // will be invoked on the IO thread.
    void GetEncryptionInfoOnUI(
        const GURL& origin,
        int64_t service_worker_registration_id,
        const std::string& sender_id,
        const PushMessagingService::EncryptionInfoCallback& io_thread_callback);

    // Called (directly) from both the UI and IO threads.
    bool is_incognito() const { return is_incognito_; }

    // Returns a push messaging service. May return null.
    PushMessagingService* service();

private:
    friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>;
    friend class base::DeleteHelper<Core>;

    ~Core();

    // Private Register methods on UI thread -------------------------------------

    void DidRequestPermissionInIncognito(const RegisterData& data,
        blink::mojom::PermissionStatus status);

    void DidRegister(const RegisterData& data,
        const std::string& push_registration_id,
        const std::vector<uint8_t>& p256dh,
        const std::vector<uint8_t>& auth,
        PushRegistrationStatus status);

    // Private Unregister methods on UI thread -----------------------------------

    void DidUnregisterFromService(int request_id,
        int64_t service_worker_registration_id,
        PushUnregistrationStatus unregistration_status);

    // Private helper methods on UI thread ---------------------------------------

    void Send(IPC::Message* message);

    // Outer part of this message filter which lives on the IO thread.
    base::WeakPtr<PushMessagingMessageFilter> io_parent_;

    int render_process_id_;

    bool is_incognito_;

    base::WeakPtrFactory<Core> weak_factory_ui_to_ui_;

    DISALLOW_COPY_AND_ASSIGN(Core);
};

PushMessagingMessageFilter::RegisterData::RegisterData()
    : request_id(0)
    , service_worker_registration_id(0)
    , render_frame_id(ChildProcessHost::kInvalidUniqueID)
{
}

bool PushMessagingMessageFilter::RegisterData::FromDocument() const
{
    return render_frame_id != ChildProcessHost::kInvalidUniqueID;
}

PushMessagingMessageFilter::Core::Core(
    const base::WeakPtr<PushMessagingMessageFilter>& io_parent,
    int render_process_id)
    : io_parent_(io_parent)
    , render_process_id_(render_process_id)
    , weak_factory_ui_to_ui_(this)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    RenderProcessHost* process_host = RenderProcessHost::FromID(render_process_id_); // Can't be null yet.
    is_incognito_ = process_host->GetBrowserContext()->IsOffTheRecord();
}

PushMessagingMessageFilter::Core::~Core() { }

PushMessagingMessageFilter::PushMessagingMessageFilter(
    int render_process_id,
    ServiceWorkerContextWrapper* service_worker_context)
    : BrowserMessageFilter(PushMessagingMsgStart)
    , service_worker_context_(service_worker_context)
    , weak_factory_io_to_io_(this)
{
    // Although this class is used only on the IO thread, it is constructed on UI.
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    // Normally, it would be unsafe to obtain a weak pointer from the UI thread,
    // but it's ok in the constructor since we can't be destroyed before our
    // constructor finishes.
    ui_core_.reset(
        new Core(weak_factory_io_to_io_.GetWeakPtr(), render_process_id));

    PushMessagingService* service = ui_core_->service();
    service_available_ = !!service;

    if (service_available_) {
        default_endpoint_ = service->GetEndpoint(false /* standard_protocol */);
        web_push_protocol_endpoint_ = service->GetEndpoint(true /* standard_protocol */);
    }
}

PushMessagingMessageFilter::~PushMessagingMessageFilter() { }

void PushMessagingMessageFilter::OnDestruct() const
{
    BrowserThread::DeleteOnIOThread::Destruct(this);
}

bool PushMessagingMessageFilter::OnMessageReceived(
    const IPC::Message& message)
{
    bool handled = true;
    IPC_BEGIN_MESSAGE_MAP(PushMessagingMessageFilter, message)
    IPC_MESSAGE_HANDLER(PushMessagingHostMsg_Subscribe, OnSubscribe)
    IPC_MESSAGE_HANDLER(PushMessagingHostMsg_Unsubscribe, OnUnsubscribe)
    IPC_MESSAGE_HANDLER(PushMessagingHostMsg_GetSubscription, OnGetSubscription)
    IPC_MESSAGE_HANDLER(PushMessagingHostMsg_GetPermissionStatus,
        OnGetPermissionStatus)
    IPC_MESSAGE_UNHANDLED(handled = false)
    IPC_END_MESSAGE_MAP()
    return handled;
}

// Subscribe methods on both IO and UI threads, merged in order of use from
// PushMessagingMessageFilter and Core.
// -----------------------------------------------------------------------------

void PushMessagingMessageFilter::OnSubscribe(
    int render_frame_id,
    int request_id,
    int64_t service_worker_registration_id,
    const PushSubscriptionOptions& options)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    // TODO(mvanouwerkerk): Validate arguments?
    RegisterData data;

    // Will be ChildProcessHost::kInvalidUniqueID in requests from Service Worker.
    data.render_frame_id = render_frame_id;

    data.request_id = request_id;
    data.service_worker_registration_id = service_worker_registration_id;
    data.options = options;

    ServiceWorkerRegistration* service_worker_registration = service_worker_context_->GetLiveRegistration(
        data.service_worker_registration_id);
    if (!service_worker_registration || !service_worker_registration->active_version()) {
        SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_NO_SERVICE_WORKER);
        return;
    }
    data.requesting_origin = service_worker_registration->pattern().GetOrigin();

    DCHECK(!(data.options.sender_info.empty() && data.FromDocument()));

    service_worker_context_->GetRegistrationUserData(
        data.service_worker_registration_id,
        { kPushRegistrationIdServiceWorkerKey, kPushSenderIdServiceWorkerKey },
        base::Bind(&PushMessagingMessageFilter::DidCheckForExistingRegistration,
            weak_factory_io_to_io_.GetWeakPtr(), data));
}

void PushMessagingMessageFilter::DidCheckForExistingRegistration(
    const RegisterData& data,
    const std::vector<std::string>& push_registration_id_and_sender_id,
    ServiceWorkerStatusCode service_worker_status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (service_worker_status == SERVICE_WORKER_OK) {
        DCHECK_EQ(2u, push_registration_id_and_sender_id.size());
        const auto& push_registration_id = push_registration_id_and_sender_id[0];
        const auto& stored_sender_id = push_registration_id_and_sender_id[1];
        std::string fixed_sender_id = FixSenderInfo(data.options.sender_info, stored_sender_id);
        if (fixed_sender_id.empty()) {
            SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_NO_SENDER_ID);
            return;
        }
        if (fixed_sender_id != stored_sender_id) {
            SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_SENDER_ID_MISMATCH);
            return;
        }
        auto callback = base::Bind(
            &PushMessagingMessageFilter::DidGetEncryptionKeys,
            weak_factory_io_to_io_.GetWeakPtr(), data, push_registration_id);
        BrowserThread::PostTask(
            BrowserThread::UI, FROM_HERE,
            base::Bind(&Core::GetEncryptionInfoOnUI,
                base::Unretained(ui_core_.get()), data.requesting_origin,
                data.service_worker_registration_id, fixed_sender_id,
                callback));
        return;
    }
    // TODO(johnme): The spec allows the register algorithm to reject with an
    // AbortError when accessing storage fails. Perhaps we should do that if
    // service_worker_status != SERVICE_WORKER_ERROR_NOT_FOUND instead of
    // attempting to do a fresh registration?
    // https://w3c.github.io/push-api/#widl-PushRegistrationManager-register-Promise-PushRegistration
    if (!data.options.sender_info.empty()) {
        BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
            base::Bind(&Core::RegisterOnUI,
                base::Unretained(ui_core_.get()), data));
    } else {
        // There is no existing registration and the sender_info passed in was
        // empty, but perhaps there is a stored sender id we can use.
        service_worker_context_->GetRegistrationUserData(
            data.service_worker_registration_id, { kPushSenderIdServiceWorkerKey },
            base::Bind(&PushMessagingMessageFilter::DidGetSenderIdFromStorage,
                weak_factory_io_to_io_.GetWeakPtr(), data));
    }
}

void PushMessagingMessageFilter::DidGetEncryptionKeys(
    const RegisterData& data,
    const std::string& push_registration_id,
    bool success,
    const std::vector<uint8_t>& p256dh,
    const std::vector<uint8_t>& auth)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (!success) {
        SendSubscriptionError(
            data, PUSH_REGISTRATION_STATUS_PUBLIC_KEY_UNAVAILABLE);
        return;
    }

    SendSubscriptionSuccess(data, PUSH_REGISTRATION_STATUS_SUCCESS_FROM_CACHE,
        push_registration_id, p256dh, auth);
}

void PushMessagingMessageFilter::DidGetSenderIdFromStorage(
    const RegisterData& data,
    const std::vector<std::string>& stored_sender_id,
    ServiceWorkerStatusCode service_worker_status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (service_worker_status != SERVICE_WORKER_OK) {
        SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_NO_SENDER_ID);
        return;
    }
    DCHECK_EQ(1u, stored_sender_id.size());
    // We should only be here because no sender info was supplied to subscribe().
    DCHECK(data.options.sender_info.empty());
    std::string fixed_sender_id = FixSenderInfo(data.options.sender_info, stored_sender_id[0]);
    if (fixed_sender_id.empty()) {
        SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_NO_SENDER_ID);
        return;
    }
    RegisterData mutated_data = data;
    mutated_data.options.sender_info = fixed_sender_id;
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::Bind(&Core::RegisterOnUI, base::Unretained(ui_core_.get()),
            mutated_data));
}

void PushMessagingMessageFilter::Core::RegisterOnUI(
    const PushMessagingMessageFilter::RegisterData& data)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    PushMessagingService* push_service = service();
    if (!push_service) {
        if (!is_incognito()) {
            // This might happen if InstanceIDProfileService::IsInstanceIDEnabled
            // returns false because the Instance ID kill switch was enabled.
            // TODO(johnme): Might be better not to expose the API in this case.
            BrowserThread::PostTask(
                BrowserThread::IO, FROM_HERE,
                base::Bind(&PushMessagingMessageFilter::SendSubscriptionError,
                    io_parent_,
                    data, PUSH_REGISTRATION_STATUS_SERVICE_NOT_AVAILABLE));
        } else {
            // Prevent websites from detecting incognito mode, by emulating what would
            // have happened if we had a PushMessagingService available.
            if (!data.FromDocument() || !data.options.user_visible_only) {
                // Throw a permission denied error under the same circumstances.
                BrowserThread::PostTask(
                    BrowserThread::IO, FROM_HERE,
                    base::Bind(&PushMessagingMessageFilter::SendSubscriptionError,
                        io_parent_, data,
                        PUSH_REGISTRATION_STATUS_INCOGNITO_PERMISSION_DENIED));
            } else {
                RenderFrameHost* render_frame_host = RenderFrameHost::FromID(render_process_id_, data.render_frame_id);
                WebContents* web_contents = WebContents::FromRenderFrameHost(render_frame_host);
                if (web_contents) {
                    web_contents->GetMainFrame()->AddMessageToConsole(
                        CONSOLE_MESSAGE_LEVEL_ERROR, kIncognitoPushUnsupportedMessage);
                    // Request push messaging permission (which will fail, since
                    // notifications aren't supported in incognito), so the website can't
                    // detect whether incognito is active.
                    web_contents->GetBrowserContext()
                        ->GetPermissionManager()
                        ->RequestPermission(
                            PermissionType::PUSH_MESSAGING, render_frame_host,
                            data.requesting_origin, false /* user_gesture */,
                            base::Bind(&PushMessagingMessageFilter::Core::
                                           DidRequestPermissionInIncognito,
                                weak_factory_ui_to_ui_.GetWeakPtr(), data));
                }
            }
        }
        return;
    }

    if (data.FromDocument()) {
        push_service->SubscribeFromDocument(
            data.requesting_origin, data.service_worker_registration_id,
            render_process_id_, data.render_frame_id, data.options,
            base::Bind(&Core::DidRegister, weak_factory_ui_to_ui_.GetWeakPtr(),
                data));
    } else {
        push_service->SubscribeFromWorker(
            data.requesting_origin, data.service_worker_registration_id,
            data.options, base::Bind(&Core::DidRegister, weak_factory_ui_to_ui_.GetWeakPtr(), data));
    }
}

void PushMessagingMessageFilter::Core::DidRequestPermissionInIncognito(
    const RegisterData& data,
    blink::mojom::PermissionStatus status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    // Notification permission should always be denied in incognito.
    DCHECK_EQ(blink::mojom::PermissionStatus::DENIED, status);
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&PushMessagingMessageFilter::SendSubscriptionError, io_parent_,
            data, PUSH_REGISTRATION_STATUS_INCOGNITO_PERMISSION_DENIED));
}

void PushMessagingMessageFilter::Core::DidRegister(
    const RegisterData& data,
    const std::string& push_registration_id,
    const std::vector<uint8_t>& p256dh,
    const std::vector<uint8_t>& auth,
    PushRegistrationStatus status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (status == PUSH_REGISTRATION_STATUS_SUCCESS_FROM_PUSH_SERVICE) {
        BrowserThread::PostTask(
            BrowserThread::IO, FROM_HERE,
            base::Bind(&PushMessagingMessageFilter::PersistRegistrationOnIO,
                io_parent_, data, push_registration_id, p256dh, auth));
    } else {
        BrowserThread::PostTask(
            BrowserThread::IO, FROM_HERE,
            base::Bind(&PushMessagingMessageFilter::SendSubscriptionError,
                io_parent_, data, status));
    }
}

void PushMessagingMessageFilter::PersistRegistrationOnIO(
    const RegisterData& data,
    const std::string& push_registration_id,
    const std::vector<uint8_t>& p256dh,
    const std::vector<uint8_t>& auth)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    service_worker_context_->StoreRegistrationUserData(
        data.service_worker_registration_id, data.requesting_origin,
        { { kPushRegistrationIdServiceWorkerKey, push_registration_id },
            { kPushSenderIdServiceWorkerKey, data.options.sender_info } },
        base::Bind(&PushMessagingMessageFilter::DidPersistRegistrationOnIO,
            weak_factory_io_to_io_.GetWeakPtr(), data,
            push_registration_id, p256dh, auth));
}

void PushMessagingMessageFilter::DidPersistRegistrationOnIO(
    const RegisterData& data,
    const std::string& push_registration_id,
    const std::vector<uint8_t>& p256dh,
    const std::vector<uint8_t>& auth,
    ServiceWorkerStatusCode service_worker_status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (service_worker_status == SERVICE_WORKER_OK) {
        SendSubscriptionSuccess(data,
            PUSH_REGISTRATION_STATUS_SUCCESS_FROM_PUSH_SERVICE,
            push_registration_id, p256dh, auth);
    } else {
        // TODO(johnme): Unregister, so PushMessagingServiceImpl can decrease count.
        SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_STORAGE_ERROR);
    }
}

void PushMessagingMessageFilter::SendSubscriptionError(
    const RegisterData& data, PushRegistrationStatus status)
{
    // Only called from IO thread, but would be safe to call from UI thread.
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (data.FromDocument()) {
        Send(new PushMessagingMsg_SubscribeFromDocumentError(
            data.render_frame_id, data.request_id, status));
    } else {
        Send(
            new PushMessagingMsg_SubscribeFromWorkerError(data.request_id, status));
    }
    RecordRegistrationStatus(status);
}

void PushMessagingMessageFilter::SendSubscriptionSuccess(
    const RegisterData& data,
    PushRegistrationStatus status,
    const std::string& push_subscription_id,
    const std::vector<uint8_t>& p256dh,
    const std::vector<uint8_t>& auth)
{
    // Only called from IO thread, but would be safe to call from UI thread.
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (!service_available_) {
        // This shouldn't be possible in incognito mode, since we've already checked
        // that we have an existing registration. Hence it's ok to throw an error.
        DCHECK(!ui_core_->is_incognito());
        SendSubscriptionError(data, PUSH_REGISTRATION_STATUS_SERVICE_NOT_AVAILABLE);
        return;
    }

    const GURL endpoint = CreateEndpoint(
        IsApplicationServerKey(data.options.sender_info), push_subscription_id);

    if (data.FromDocument()) {
        Send(new PushMessagingMsg_SubscribeFromDocumentSuccess(
            data.render_frame_id, data.request_id, endpoint, data.options, p256dh,
            auth));
    } else {
        Send(new PushMessagingMsg_SubscribeFromWorkerSuccess(
            data.request_id, endpoint, data.options, p256dh, auth));
    }
    RecordRegistrationStatus(status);
}

// Unsubscribe methods on both IO and UI threads, merged in order of use from
// PushMessagingMessageFilter and Core.
// -----------------------------------------------------------------------------

void PushMessagingMessageFilter::OnUnsubscribe(
    int request_id, int64_t service_worker_registration_id)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    ServiceWorkerRegistration* service_worker_registration = service_worker_context_->GetLiveRegistration(
        service_worker_registration_id);
    if (!service_worker_registration) {
        DidUnregister(request_id, PUSH_UNREGISTRATION_STATUS_NO_SERVICE_WORKER);
        return;
    }

    service_worker_context_->GetRegistrationUserData(
        service_worker_registration_id, { kPushSenderIdServiceWorkerKey },
        base::Bind(&PushMessagingMessageFilter::UnsubscribeHavingGottenSenderId,
            weak_factory_io_to_io_.GetWeakPtr(), request_id,
            service_worker_registration_id,
            service_worker_registration->pattern().GetOrigin()));
}

void PushMessagingMessageFilter::UnsubscribeHavingGottenSenderId(
    int request_id,
    int64_t service_worker_registration_id,
    const GURL& requesting_origin,
    const std::vector<std::string>& sender_ids,
    ServiceWorkerStatusCode service_worker_status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    std::string sender_id;
    if (service_worker_status == SERVICE_WORKER_OK) {
        DCHECK_EQ(1u, sender_ids.size());
        sender_id = sender_ids[0];
    }
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::Bind(&Core::UnregisterFromService, base::Unretained(ui_core_.get()),
            request_id, service_worker_registration_id, requesting_origin,
            sender_id));
}

void PushMessagingMessageFilter::Core::UnregisterFromService(
    int request_id,
    int64_t service_worker_registration_id,
    const GURL& requesting_origin,
    const std::string& sender_id)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    PushMessagingService* push_service = service();
    if (!push_service) {
        // This shouldn't be possible in incognito mode, since we've already checked
        // that we have an existing registration. Hence it's ok to throw an error.
        DCHECK(!is_incognito());
        BrowserThread::PostTask(
            BrowserThread::IO, FROM_HERE,
            base::Bind(&PushMessagingMessageFilter::DidUnregister, io_parent_,
                request_id,
                PUSH_UNREGISTRATION_STATUS_SERVICE_NOT_AVAILABLE));
        return;
    }

    push_service->Unsubscribe(
        requesting_origin, service_worker_registration_id, sender_id,
        base::Bind(&Core::DidUnregisterFromService,
            weak_factory_ui_to_ui_.GetWeakPtr(), request_id,
            service_worker_registration_id));
}

void PushMessagingMessageFilter::Core::DidUnregisterFromService(
    int request_id,
    int64_t service_worker_registration_id,
    PushUnregistrationStatus unregistration_status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&PushMessagingMessageFilter::DidUnregister, io_parent_,
            request_id, unregistration_status));
}

void PushMessagingMessageFilter::DidUnregister(
    int request_id,
    PushUnregistrationStatus unregistration_status)
{
    // Only called from IO thread, but would be safe to call from UI thread.
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    switch (unregistration_status) {
    case PUSH_UNREGISTRATION_STATUS_SUCCESS_UNREGISTERED:
    case PUSH_UNREGISTRATION_STATUS_PENDING_NETWORK_ERROR:
    case PUSH_UNREGISTRATION_STATUS_PENDING_SERVICE_ERROR:
        Send(new PushMessagingMsg_UnsubscribeSuccess(request_id, true));
        break;
    case PUSH_UNREGISTRATION_STATUS_SUCCESS_WAS_NOT_REGISTERED:
        Send(new PushMessagingMsg_UnsubscribeSuccess(request_id, false));
        break;
    case PUSH_UNREGISTRATION_STATUS_NO_SERVICE_WORKER:
    case PUSH_UNREGISTRATION_STATUS_SERVICE_NOT_AVAILABLE:
    case PUSH_UNREGISTRATION_STATUS_STORAGE_ERROR:
        Send(new PushMessagingMsg_UnsubscribeError(
            request_id, blink::WebPushError::ErrorTypeAbort,
            PushUnregistrationStatusToString(unregistration_status)));
        break;
    case PUSH_UNREGISTRATION_STATUS_NETWORK_ERROR:
        NOTREACHED();
        break;
    }
    RecordUnregistrationStatus(unregistration_status);
}

// GetSubscription methods on both IO and UI threads, merged in order of use
// from PushMessagingMessageFilter and Core.
// -----------------------------------------------------------------------------

void PushMessagingMessageFilter::OnGetSubscription(
    int request_id,
    int64_t service_worker_registration_id)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    // TODO(johnme): Validate arguments?
    service_worker_context_->GetRegistrationUserData(
        service_worker_registration_id,
        { kPushRegistrationIdServiceWorkerKey, kPushSenderIdServiceWorkerKey },
        base::Bind(&PushMessagingMessageFilter::DidGetSubscription,
            weak_factory_io_to_io_.GetWeakPtr(), request_id,
            service_worker_registration_id));
}

void PushMessagingMessageFilter::DidGetSubscription(
    int request_id,
    int64_t service_worker_registration_id,
    const std::vector<std::string>& push_subscription_id_and_sender_info,
    ServiceWorkerStatusCode service_worker_status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    PushGetRegistrationStatus get_status = PUSH_GETREGISTRATION_STATUS_STORAGE_ERROR;
    switch (service_worker_status) {
    case SERVICE_WORKER_OK: {
        DCHECK_EQ(2u, push_subscription_id_and_sender_info.size());

        if (!service_available_) {
            // Return not found in incognito mode, so websites can't detect it.
            get_status = ui_core_->is_incognito()
                ? PUSH_GETREGISTRATION_STATUS_INCOGNITO_REGISTRATION_NOT_FOUND
                : PUSH_GETREGISTRATION_STATUS_SERVICE_NOT_AVAILABLE;
            break;
        }

        ServiceWorkerRegistration* registration = service_worker_context_->GetLiveRegistration(
            service_worker_registration_id);
        const GURL origin = registration->pattern().GetOrigin();

        const bool uses_standard_protocol = IsApplicationServerKey(push_subscription_id_and_sender_info[1]);
        const GURL endpoint = CreateEndpoint(
            uses_standard_protocol, push_subscription_id_and_sender_info[0]);

        auto callback = base::Bind(&PushMessagingMessageFilter::DidGetSubscriptionKeys,
            weak_factory_io_to_io_.GetWeakPtr(), request_id, endpoint,
            push_subscription_id_and_sender_info[1]);

        BrowserThread::PostTask(
            BrowserThread::UI, FROM_HERE,
            base::Bind(&Core::GetEncryptionInfoOnUI,
                base::Unretained(ui_core_.get()), origin,
                service_worker_registration_id,
                push_subscription_id_and_sender_info[1], callback));

        return;
    }
    case SERVICE_WORKER_ERROR_NOT_FOUND: {
        get_status = PUSH_GETREGISTRATION_STATUS_REGISTRATION_NOT_FOUND;
        break;
    }
    case SERVICE_WORKER_ERROR_FAILED: {
        get_status = PUSH_GETREGISTRATION_STATUS_STORAGE_ERROR;
        break;
    }
    case SERVICE_WORKER_ERROR_ABORT:
    case SERVICE_WORKER_ERROR_START_WORKER_FAILED:
    case SERVICE_WORKER_ERROR_PROCESS_NOT_FOUND:
    case SERVICE_WORKER_ERROR_EXISTS:
    case SERVICE_WORKER_ERROR_INSTALL_WORKER_FAILED:
    case SERVICE_WORKER_ERROR_ACTIVATE_WORKER_FAILED:
    case SERVICE_WORKER_ERROR_IPC_FAILED:
    case SERVICE_WORKER_ERROR_NETWORK:
    case SERVICE_WORKER_ERROR_SECURITY:
    case SERVICE_WORKER_ERROR_EVENT_WAITUNTIL_REJECTED:
    case SERVICE_WORKER_ERROR_STATE:
    case SERVICE_WORKER_ERROR_TIMEOUT:
    case SERVICE_WORKER_ERROR_SCRIPT_EVALUATE_FAILED:
    case SERVICE_WORKER_ERROR_DISK_CACHE:
    case SERVICE_WORKER_ERROR_REDUNDANT:
    case SERVICE_WORKER_ERROR_DISALLOWED:
    case SERVICE_WORKER_ERROR_MAX_VALUE: {
        NOTREACHED() << "Got unexpected error code: " << service_worker_status
                     << " " << ServiceWorkerStatusToString(service_worker_status);
        get_status = PUSH_GETREGISTRATION_STATUS_STORAGE_ERROR;
        break;
    }
    }
    Send(new PushMessagingMsg_GetSubscriptionError(request_id, get_status));
    RecordGetRegistrationStatus(get_status);
}

void PushMessagingMessageFilter::DidGetSubscriptionKeys(
    int request_id,
    const GURL& endpoint,
    const std::string& sender_info,
    bool success,
    const std::vector<uint8_t>& p256dh,
    const std::vector<uint8_t>& auth)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (!success) {
        PushGetRegistrationStatus status = PUSH_GETREGISTRATION_STATUS_PUBLIC_KEY_UNAVAILABLE;

        Send(new PushMessagingMsg_GetSubscriptionError(request_id, status));

        RecordGetRegistrationStatus(status);
        return;
    }

    PushSubscriptionOptions options;
    // Chrome rejects subscription requests with userVisibleOnly false, so it must
    // have been true. TODO(harkness): If Chrome starts accepting silent push
    // subscriptions with userVisibleOnly false, the bool will need to be stored.
    options.user_visible_only = true;
    options.sender_info = sender_info;

    Send(new PushMessagingMsg_GetSubscriptionSuccess(request_id, endpoint,
        options, p256dh, auth));

    RecordGetRegistrationStatus(PUSH_GETREGISTRATION_STATUS_SUCCESS);
}

// GetPermission methods on both IO and UI threads, merged in order of use from
// PushMessagingMessageFilter and Core.
// -----------------------------------------------------------------------------

void PushMessagingMessageFilter::OnGetPermissionStatus(
    int request_id,
    int64_t service_worker_registration_id,
    bool user_visible)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    ServiceWorkerRegistration* service_worker_registration = service_worker_context_->GetLiveRegistration(
        service_worker_registration_id);
    if (!service_worker_registration) {
        Send(new PushMessagingMsg_GetPermissionStatusError(
            request_id, blink::WebPushError::ErrorTypeAbort));
        return;
    }

    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::Bind(&Core::GetPermissionStatusOnUI,
            base::Unretained(ui_core_.get()),
            service_worker_registration->pattern().GetOrigin(),
            user_visible, request_id));
}

void PushMessagingMessageFilter::Core::GetPermissionStatusOnUI(
    const GURL& requesting_origin, bool user_visible, int request_id)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    blink::WebPushPermissionStatus permission_status;
    PushMessagingService* push_service = service();
    if (push_service) {
        if (!user_visible && !push_service->SupportNonVisibleMessages()) {
            Send(new PushMessagingMsg_GetPermissionStatusError(
                request_id, blink::WebPushError::ErrorTypeNotSupported));
            return;
        }
        permission_status = push_service->GetPermissionStatus(requesting_origin, user_visible);
    } else if (is_incognito()) {
        // Return prompt, so the website can't detect incognito mode.
        permission_status = blink::WebPushPermissionStatusPrompt;
    } else {
        Send(new PushMessagingMsg_GetPermissionStatusError(
            request_id, blink::WebPushError::ErrorTypeAbort));
        return;
    }
    Send(new PushMessagingMsg_GetPermissionStatusSuccess(request_id,
        permission_status));
}

// Helper methods on both IO and UI threads, merged from
// PushMessagingMessageFilter and Core.
// -----------------------------------------------------------------------------

void PushMessagingMessageFilter::Core::GetEncryptionInfoOnUI(
    const GURL& origin,
    int64_t service_worker_registration_id,
    const std::string& sender_id,
    const PushMessagingService::EncryptionInfoCallback& io_thread_callback)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    PushMessagingService* push_service = service();
    if (push_service) {
        push_service->GetEncryptionInfo(
            origin, service_worker_registration_id, sender_id,
            base::Bind(&ForwardEncryptionInfoToIOThreadProxy, io_thread_callback));
        return;
    }

    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
        base::Bind(io_thread_callback, false /* success */,
            std::vector<uint8_t>() /* p256dh */,
            std::vector<uint8_t>() /* auth */));
}

void PushMessagingMessageFilter::Core::Send(IPC::Message* message)
{
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&PushMessagingMessageFilter::SendIPC, io_parent_,
            base::Passed(base::WrapUnique(message))));
}

void PushMessagingMessageFilter::SendIPC(
    std::unique_ptr<IPC::Message> message)
{
    Send(message.release());
}

GURL PushMessagingMessageFilter::CreateEndpoint(
    bool standard_protocol,
    const std::string& subscription_id) const
{
    const GURL& base = standard_protocol ? web_push_protocol_endpoint_ : default_endpoint_;

    return GURL(base.spec() + subscription_id);
}

PushMessagingService* PushMessagingMessageFilter::Core::service()
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    RenderProcessHost* process_host = RenderProcessHost::FromID(render_process_id_);
    return process_host
        ? process_host->GetBrowserContext()->GetPushMessagingService()
        : nullptr;
}

} // namespace content
